package org.tech4j.springboot.starter.logging.common.aop;

import com.google.common.collect.Maps;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindingResult;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import org.tech4j.common.utils.DateUtils;
import org.tech4j.common.utils.JsonUtils;
import org.tech4j.logging.common.annotation.Tech4jLog;
import org.tech4j.logging.common.collector.LogCollector;
import org.tech4j.logging.common.entity.LogDetail;
import org.tech4j.springboot.starter.logging.common.context.LogAttributes;
import org.tech4j.springboot.starter.logging.common.context.LogContextHolder;

import java.lang.reflect.Array;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.IntStream;

/**
 * @program: tech4j
 * @description:
 * @author: YuKai Fan
 * @create: 2025/3/5 21:51
 **/
@Aspect
public class LogAspect {
    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);

    private final LogCollector<LogDetail> logCollector;

    public LogAspect(LogCollector<LogDetail> logCollector) {
        this.logCollector = logCollector;
    }

    @Around("@annotation(tech4jLog)")
    public Object around(ProceedingJoinPoint joinPoint, Tech4jLog tech4jLog) throws Throwable {
        return around0(joinPoint, tech4jLog);
    }

    private Object around0(ProceedingJoinPoint joinPoint, Tech4jLog tech4jLog) throws Throwable {
        LocalDateTime startTime = LocalDateTime.now();
        try {
            // 执行原有方法
            Object result = joinPoint.proceed();
            // 记录正常执行的操作日志
            this.log(joinPoint, tech4jLog, startTime, result, null);
            return result;
        } catch (Throwable ex) {
            this.log(joinPoint, tech4jLog, startTime, null, ex);
            throw ex;
        } finally {
            clearThreadLocal();
        }
    }

    private void log(ProceedingJoinPoint joinPoint, Tech4jLog tech4jLog, LocalDateTime startTime, Object result, Throwable exception) {
        try {
            // 判断不记录的情况
            if (!isLogEnable(joinPoint, tech4jLog)) {
                return;
            }
            // 真正记录操作日志
            this.log0(joinPoint, tech4jLog, startTime, result, exception);
        } catch (Throwable ex) {
            log.error("[log][记录操作日志时，发生异常，其中参数是 joinPoint({}) tech4jLog({}) result({}) exception({}) ]",
                    joinPoint, tech4jLog, result, exception, ex);
        }
    }

    private void log0(ProceedingJoinPoint joinPoint, Tech4jLog tech4jLog, LocalDateTime startTime, Object result, Throwable exception) {
        LogDetail logDetail = new LogDetail();
        logDetail.setStartTime(startTime);
        // 补充用户信息
        fillUserFields(logDetail);
        // 补全模块信息
        fillModuleFields(logDetail, tech4jLog);
        // 补全请求信息
        fillRequestFields(logDetail);
        // 补全方法信息
        fillMethodFields(logDetail, joinPoint, tech4jLog, startTime, result, exception);

        // 记录日志
        logCollector.collect(logDetail);
    }

    private void fillMethodFields(LogDetail logDetail, ProceedingJoinPoint joinPoint, Tech4jLog tech4jLog, LocalDateTime startTime, Object result, Throwable exception) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        logDetail.setJavaMethod(methodSignature.toShortString());
        if (tech4jLog.logArgs()) {
            logDetail.setJavaMethodArgs(obtainMethodArgs(joinPoint));
        }
        if (tech4jLog.logResultData()) {
            logDetail.setResultData(JsonUtils.toJsonString(result));
        }
        logDetail.setDuration(DateUtils.acquireMillisBetween(startTime, LocalDateTime.now()));
    }

    private void fillRequestFields(LogDetail logDetail) {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (!(requestAttributes instanceof ServletRequestAttributes)) {
            return;
        }
        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
        logDetail.setRequestMethod(request.getMethod());
        logDetail.setRequestUrl(request.getRequestURI());
    }

    private void fillUserFields(LogDetail logDetail) {
        LogAttributes logAttributes = LogContextHolder.getLogAttributes();
        if (Objects.isNull(logAttributes)) {
            return;
        }
        logDetail.setUserId(logAttributes.getUserIdAttr());
        logDetail.setOperator(logAttributes.getOperatorAttr());
        logDetail.setTenantId(logAttributes.getTenantIdAttr());
        logDetail.setTraceId(logAttributes.getTraceIdAttr());
    }

    private void fillModuleFields(LogDetail logDetail, Tech4jLog tech4jLog) {
        logDetail.setModuleType(tech4jLog.moduleType());
        logDetail.setModuleName(tech4jLog.moduleName());
        logDetail.setName(tech4jLog.name());

        LogAttributes logAttributes = LogContextHolder.getLogAttributes();
        if (Objects.isNull(logAttributes)) {
            return;
        }
        logDetail.setExt(logAttributes.getAttachments());
    }

    private static String obtainMethodArgs(ProceedingJoinPoint joinPoint) {
        try {
            // TODO 提升：参数脱敏和忽略
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            String[] argNames = methodSignature.getParameterNames();
            Object[] argValues = joinPoint.getArgs();
            // 拼接参数
            Map<String, Object> args = Maps.newHashMapWithExpectedSize(argValues.length);
            for (int i = 0; i < argNames.length; i++) {
                String argName = argNames[i];
                Object argValue = argValues[i];
                // 被忽略时，标记为 ignore 字符串，避免和 null 混在一起
                args.put(argName, !isIgnoreArgs(argValue) ? argValue : "[ignore]");
            }
            return JsonUtils.toJsonString(args);
        } catch (Exception e) {
            log.error("请求参数无法转为Json");
            return "请求参数无法转为字符串";
        }
    }

    private static boolean isIgnoreArgs(Object object) {
        Class<?> clazz = object.getClass();
        // 处理数组的情况
        if (clazz.isArray()) {
            return IntStream.range(0, Array.getLength(object))
                    .anyMatch(index -> isIgnoreArgs(Array.get(object, index)));
        }
        // 递归，处理数组、Collection、Map 的情况
        if (Collection.class.isAssignableFrom(clazz)) {
            return ((Collection<?>) object).stream()
                    .anyMatch((Predicate<Object>) LogAspect::isIgnoreArgs);
        }
        if (Map.class.isAssignableFrom(clazz)) {
            return isIgnoreArgs(((Map<?, ?>) object).values());
        }
        // obj
        return object instanceof MultipartFile
                || object instanceof HttpServletRequest
                || object instanceof HttpServletResponse
                || object instanceof BindingResult;
    }


    /**
     * 是否开启日志
     * @param joinPoint
     * @param tech4jLog
     * @return
     */
    private boolean isLogEnable(ProceedingJoinPoint joinPoint, Tech4jLog tech4jLog) {
        return tech4jLog.enable();
    }

    private static void clearThreadLocal() {
        LogContextHolder.resetLogAttributes();
    }
}
